Imports Microsoft.ManagementConsole
Imports System.Windows.Forms
Imports System.ComponentModel
Imports System
Imports System.Security.Permissions
Imports System.DirectoryServices
Imports System.DirectoryServices.ActiveDirectory

Public Class MMCUserControl_ALM
  Implements IFormViewControl

#Region " Global Variables "
  Private _curDomain As String = ""
  Private _curNode As TreeNode = Nothing
  Private _recurseNodes As Boolean = False
  Private _treeViewThread As System.Threading.Thread = Nothing
  Private _listViewThread As System.Threading.Thread = Nothing
  Private _bmCheck As New Drawing.Bitmap(My.Resources.Check)
  Private _bmWarn As New Drawing.Bitmap(My.Resources.Warn)
  Private _bmHalfHeight As Integer = 0
  Private _bmHalfWidth As Integer = 0
#End Region

#Region " Constants "
  ' These index numbers correspond to the image position in the image list
  Private Const IM_DOMAIN As Integer = 0
  Private Const IM_CONTAINER As Integer = 1
  Private Const IM_OU As Integer = 2

  ' The following constants represent the string value in listviewsubitems that correspond to
  ' certain bitmaps to show instead of text (the alphabetical value is important for sorting)
  Private Const SI_CHECKED As String = "A"
  Private Const SI_UNCHECKED As String = "B"
  Private Const SI_UNKNOWN As String = "C"

  Public Enum AdsUserFlags
    Script = 1
    AccountDisabled = 2
    HomeDirectoryRequired = 8
    AccountLockedOut = 16
    PasswordNotRequired = 32
    PasswordCannotChange = 64
    EncryptedTextPasswordAllowed = 128
    TempDuplicateAccount = 256
    NormalAccount = 512
    InterDomainTrustAccount = 2048
    WorkstationTrustAccount = 4096
    ServerTrustAccount = 8192
    PasswordDoesNotExpire = 65536
    MnsLogonAccount = 131072
    SmartCardRequired = 262144
    TrustedForDelegation = 524288
    AccountNotDelegated = 1048576
    UseDesKeyOnly = 2097152
    DontRequirePreauth = 4194304
    PasswordExpired = 8388608
    TrustedToAuthenticateForDelegation = 16777216
    NoAuthDataRequired = 33554432
  End Enum
#End Region

#Region " Construction / Initialization "
  Public Sub New()
    Me.InitializeComponent()

    ' Set color transparency for the icons...
    Me._bmCheck.MakeTransparent(Drawing.Color.FromArgb(255, 255, 255))
    Me._bmWarn.MakeTransparent(Drawing.Color.FromArgb(255, 255, 255))
    Me._bmHalfWidth = Me._bmCheck.Width / 2
    Me._bmHalfHeight = Me._bmCheck.Height / 2

    ' find all the forests and launch the thread to load the default domain
    Try
      For Each d As String In MMCUserControl_ALM.GetDomainsInCurrentForest
        Me.ToolStripComboBoxDomainSelection.Items.Add(d)
        Me.ToolStripComboBoxDomainSelection.AutoCompleteCustomSource.Add(d)
      Next

      If Me.ToolStripComboBoxDomainSelection.Items.Count > 0 Then
        Me.ToolStripComboBoxDomainSelection.Text = Me.ToolStripComboBoxDomainSelection.Items(0)
        Me.ToolStripButtonOpenDomain.Enabled = False
        Me.LaunchTreeViewRebuildThread()
      End If

    Catch ex As Exception
      MB("Unable to enumerate domains in current forest (" & ex.GetType.ToString & "): " & ex.Message, "Account Lockout Management", MessageBoxButtons.OK, MessageBoxIcon.Error)
    End Try
  End Sub

  Public Sub Initialize(ByVal view As Microsoft.ManagementConsole.FormView) Implements Microsoft.ManagementConsole.IFormViewControl.Initialize
  End Sub
#End Region

#Region " Domain / Forest Helper Functions "
  ''' <summary>
  ''' Returns all domains (and subdomains) in a forest
  ''' </summary>
  ''' <returns>A List(Of String) collection of the names of all domains in the forest</returns>
  Public Shared Function GetDomainsInCurrentForest() As List(Of String)
    GetDomainsInCurrentForest = New List(Of String)

    For Each d As Domain In Forest.GetCurrentForest.Domains
      GetDomainsInCurrentForest.Add(d.Name)
      GetDomainsInCurrentForest.AddRange(MMCUserControl_ALM.GetRecursedSubDomainsInDomain(d))
    Next
  End Function

  ''' <summary>
  ''' Returns all subdomains of a domain, recursively
  ''' </summary>
  ''' <param name="dom">The domain whose child domains we want to find</param>
  ''' <returns>A List(Of String) collection of the names of all subdomains in the specified domain</returns>
  Public Shared Function GetRecursedSubDomainsInDomain(ByVal dom As Domain) As List(Of String)
    GetRecursedSubDomainsInDomain = New List(Of String)

    If dom IsNot Nothing Then
      For Each c As Domain In dom.Children
        GetRecursedSubDomainsInDomain.Add(c.Name)
        GetRecursedSubDomainsInDomain.AddRange(MMCUserControl_ALM.GetRecursedSubDomainsInDomain(c))
      Next
    End If
  End Function


  Private Sub RebuildDomainTreeViewThread()
    Try
      ' Fetch current domain
      Dim curDomain As String = ""
      SyncLock Me._curDomain
        curDomain = Me._curDomain
      End SyncLock

      If curDomain.Length > 0 Then
        Try
          ' Clear out the current node lists
          TreeViewClearNodes(Me.TreeViewDomains)
          ClearListView(Me.ListViewUsers)

          ' Create the root node
          Dim rootNode As New TreeNode(curDomain)

          ' Get the target domain context
          Dim targetDomain As Domain = Domain.GetDomain(New DirectoryContext(DirectoryContextType.Domain, curDomain))

          ' Get the directory entry for the target domain
          Dim rootEntry As DirectoryEntry = targetDomain.GetDirectoryEntry()

          ' Set the rootnode to the correct icon
          rootNode.ImageIndex = IM_DOMAIN

          ' Populate the treeview
          Me.RebuildDomainTreeViewNextNode(rootNode, rootEntry)

          ' expand the root node
          rootNode.Expand()

          ' Add the node to the treeview
          TreeViewAddRootNode(Me.TreeViewDomains, rootNode)

          ' Select the root node
          Me.TreeViewSelectNode(Me.TreeViewDomains, rootNode)
        Catch ex As Threading.ThreadAbortException
          Throw
        Catch ex As Exception
          TreeViewClearNodes(Me.TreeViewDomains)
          ClearListView(Me.ListViewUsers)
          'TODO: clear out bottom pane too
          MB("Unable to load selected domain (" & ex.GetType.ToString & "): " & ex.Message, "Account Lockout Management", MessageBoxButtons.OK, MessageBoxIcon.Error)
        Finally
          ' Set visibility
          Me.SetToolStripItemVisible(Me.ToolStripDomain, Me.ToolStripButtonOpenDomain, True)
          Me.SetToolStripItemEnabled(Me.ToolStripDomain, Me.ToolStripComboBoxDomainSelection, True)
          Me.SetToolStripItemEnabled(Me.ToolStripDomain, Me.ToolStripButtonStopDomainLoad, False)
          Me.SetToolStripItemVisible(Me.ToolStripDomain, Me.ToolStripButtonStopDomainLoad, False)
          Me.SetControlEnabled(Me.TreeViewDomains, True)
          Me.SetControlVisible(Me.LabelLoadingDomain, False)
          Me.SetControlEnabled(Me.LabelLoadingDomain, False)
        End Try
      End If
    Catch ex As Threading.ThreadAbortException
      Me.SetToolStripItemEnabled(Me.ToolStripDomain, Me.ToolStripButtonOpenDomain, True)
      Throw
    End Try
  End Sub

  Private Sub RebuildDomainTreeViewNextNode(ByVal node As TreeNode, ByVal entry As DirectoryEntry)
    For Each child As DirectoryEntry In entry.Children
      If child.SchemaClassName.ToUpper = "ORGANIZATIONALUNIT" Then
        ' Fetch the object name and strip off the leading distinguished name part
        Dim name As String = child.Name
        If name.Length > 3 AndAlso Microsoft.VisualBasic.Left(name, 3).ToUpper = "OU=" Then
          name = name.Substring(3)
        End If

        ' Define the treenode for the treeview (name of node, plus unselected/selected imageindex)
        Dim nextNode As New TreeNode(name, IM_OU, IM_OU)

        ' Add the node
        node.Nodes.Add(nextNode)

        ' Recurse to the children
        Me.RebuildDomainTreeViewNextNode(nextNode, child)
      ElseIf child.SchemaClassName.ToUpper = "CONTAINER" Then
        ' Fetch the object name and strip off the leading distinguished name part
        Dim name As String = child.Name
        If name.Length > 3 AndAlso Microsoft.VisualBasic.Left(name, 3).ToUpper = "CN=" Then
          name = name.Substring(3)
        End If

        ' Define the treenode for the treeview (name of node, plus unselected/selected imageindex)
        Dim nextNode As New TreeNode(name, IM_CONTAINER, IM_CONTAINER)

        ' Add the node
        node.Nodes.Add(nextNode)

        ' Recurse to the children
        Me.RebuildDomainTreeViewNextNode(nextNode, child)
      End If
    Next
  End Sub

  Private Shared Function ConvertDomainNameToDistinguishedName(ByVal dom As String) As String
    ConvertDomainNameToDistinguishedName = ""

    For Each s As String In dom.Trim.Split(".")
      If ConvertDomainNameToDistinguishedName <> "" Then
        ConvertDomainNameToDistinguishedName &= ","
      End If
      ConvertDomainNameToDistinguishedName &= "DC=" & s
    Next
  End Function

  Private Shared Function GetContainerDistinguishedNameAtNode(ByVal node As TreeNode) As String
    GetContainerDistinguishedNameAtNode = ""

    Dim curNode As TreeNode = node

    While curNode IsNot Nothing
      If curNode.ImageIndex = IM_OU Then
        If GetContainerDistinguishedNameAtNode <> "" Then
          GetContainerDistinguishedNameAtNode &= ","
        End If
        GetContainerDistinguishedNameAtNode &= "OU=" & curNode.Text
      ElseIf curNode.ImageIndex = IM_CONTAINER Then
        If GetContainerDistinguishedNameAtNode <> "" Then
          GetContainerDistinguishedNameAtNode &= ","
        End If
        GetContainerDistinguishedNameAtNode &= "CN=" & curNode.Text
      Else ' We're into the "DC" part now
        Exit While
      End If
      curNode = curNode.Parent
    End While
  End Function
#End Region

#Region " User List Helper Functions "
  Public Sub RebuildUserListViewThread()
    Try
      Dim curDomain As String = ""
      Dim curNode As TreeNode = Nothing
      Dim recurse As Boolean = False

      SyncLock Me._curNode
        curDomain = Me._curDomain
        curNode = Me._curNode
        recurse = Me._recurseNodes
      End SyncLock

      Me.ClearListView(Me.ListViewUsers)

      'For Each lvi As ListViewItem In MMCUserControl_ALM.GetListOfUsers(curDomain, curNode, recurse)
      Me.AddRangeToListView(Me.ListViewUsers, MMCUserControl_ALM.GetListOfUsers(curDomain, curNode, recurse).ToArray)
      'Next

      Me.SetControlEnabled(Me.ListViewUsers, True)
      Me.SetControlEnabled(Me.ToolStripUsers, True)
      Me.SetControlVisible(Me.LabelLoadingUsers, False)
      Me.SetControlEnabled(Me.LabelLoadingUsers, False)
    Catch ex As Threading.ThreadAbortException
      Throw
    Catch ex As Exception
      Me.ClearListView(Me.ListViewUsers)
      MB("Unable to load users from selected container (" & ex.GetType.ToString & "): " & ex.Message, "Account Lockout Management", MessageBoxButtons.OK, MessageBoxIcon.Error)
    End Try
  End Sub


  Public Shared Function GetListOfUsers(ByVal dom As String, ByVal node As TreeNode, ByVal recurse As Boolean) As List(Of ListViewItem)
    ' Initialize the list
    GetListOfUsers = New List(Of ListViewItem)

    ' Convert the domain name to canonical context
    Dim dnDom As String = MMCUserControl_ALM.ConvertDomainNameToDistinguishedName(dom)
    Dim dnPath As String = MMCUserControl_ALM.GetContainerDistinguishedNameAtNode(node)
    Dim dnNode As String = ""

    If dnPath <> "" Then
      dnNode = dnPath & "," & dnDom
    Else
      dnNode = dnDom
    End If

    ' Look up the container in AD

    If recurse Then
      Dim dir As New DirectoryEntry("LDAP://" & dnDom)

      Dim srch As New DirectorySearcher(dir)
      srch.Filter = "(objectClass=User)"
      srch.PropertiesToLoad.Add("samaccountname")
      srch.PropertiesToLoad.Add("distinguishedname")
      srch.PropertiesToLoad.Add("userAccountControl")
      srch.PropertiesToLoad.Add("displayname")
      srch.PropertiesToLoad.Add("msDS-User-Account-Control-Computed")
      srch.PropertiesToLoad.Add("logonhours")

      For Each user As SearchResult In srch.FindAll()
        If Microsoft.VisualBasic.Right(user.Properties("distinguishedname")(0).ToString, dnNode.Length).ToLower = dnNode.ToLower Then
          GetListOfUsers.Add(BuildUserLVI(user.GetDirectoryEntry))
        End If
      Next
    Else
      Dim dir As New DirectoryEntry("LDAP://" & dnNode)

      For Each user As DirectoryEntry In dir.Children
        ' process all the properties
        For Each oc As String In user.Properties("objectclass")
          If oc.ToLower = "user" Then
            GetListOfUsers.Add(BuildUserLVI(user))
            Exit For
          End If
        Next
      Next
    End If

  End Function

  Private Shared Function BuildUserLVI(ByVal user As DirectoryEntry) As ListViewItem
    ' Refresh account flags
    user.RefreshCache(New String() {"msDS-User-Account-Control-Computed"})

    ' Load simple names
    Dim lvi As New ListViewItem(GetDEPropertyString(user, "samaccountname"))
    lvi.SubItems.Add(GetDEPropertyString(user, "displayname"))

    ' Load flags
    If user.Properties("userAccountControl").Count > 0 Then
      Dim flags As Integer = CInt(user.Properties("userAccountControl")(0))

      If (flags And AdsUserFlags.AccountDisabled) = AdsUserFlags.AccountDisabled Then
        lvi.SubItems.Add(SI_CHECKED)
      Else
        lvi.SubItems.Add(SI_UNCHECKED)
      End If

      If (flags And AdsUserFlags.AccountLockedOut) = AdsUserFlags.AccountLockedOut Then
        lvi.SubItems.Add(SI_CHECKED)
      Else
        lvi.SubItems.Add(SI_UNCHECKED)
      End If
    Else
      lvi.SubItems.Add(SI_UNKNOWN)
      lvi.SubItems.Add(SI_UNKNOWN)
    End If

    If user.Properties("msDS-User-Account-Control-Computed").Count > 0 Then
      Dim xflags As Integer = CInt(user.Properties("msDS-User-Account-Control-Computed")(0))

      If (xflags And AdsUserFlags.PasswordExpired) = AdsUserFlags.PasswordExpired Then
        lvi.SubItems.Add(SI_CHECKED)
      Else
        lvi.SubItems.Add(SI_UNCHECKED)
      End If
    Else
      lvi.SubItems.Add(SI_UNKNOWN)
    End If

    Try
      If HasNoLogonHours(user) Then
        lvi.SubItems.Add(SI_CHECKED)
      Else
        lvi.SubItems.Add(SI_UNCHECKED)
      End If
    Catch ex As Exception
      lvi.SubItems.Add(SI_UNKNOWN)
    End Try

    Return lvi
  End Function

  Private Shared Function GetDEPropertyString(ByVal de As DirectoryEntry, ByVal prop As String) As String
    If de.Properties(prop).Count > 0 Then
      Return de.Properties(prop)(0).ToString
    Else
      Return ""
    End If
  End Function

  Private Shared Function HasNoLogonHours(ByVal de As DirectoryEntry) As Boolean
    If de.Properties("logonhours").Count > 0 Then
      For Each i As Integer In de.Properties("logonhours")(0)
        If i > 0 Then
          Return False
        End If
      Next

      Return True
    Else
      Throw New Exception("Unknown")
    End If
  End Function


#End Region

#Region " Event Handlers "
  Private Sub ToolStripComboBoxDomainSelection_KeyDown_KeyUp(ByVal sender As Object, ByVal e As KeyEventArgs) Handles ToolStripComboBoxDomainSelection.KeyDown, ToolStripComboBoxDomainSelection.KeyUp
    If e.KeyData = Keys.Enter AndAlso Me.ToolStripComboBoxDomainSelection.Text.Trim.Length > 0 Then
      Me.ToolStripButtonOpenDomain.Enabled = False
      Me.LaunchTreeViewRebuildThread()
    ElseIf Me.ToolStripComboBoxDomainSelection.Text.Trim.Length > 0 Then
      Me.ToolStripButtonOpenDomain.Enabled = True
    Else
      Me.ToolStripButtonOpenDomain.Enabled = False
    End If
  End Sub

  Private Sub ToolStripComboBoxDomainSelection_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs) Handles ToolStripComboBoxDomainSelection.SelectedIndexChanged
    If Me._curDomain.Trim.ToUpper <> Me.ToolStripComboBoxDomainSelection.Text.Trim.ToUpper Then
      Me.ToolStripButtonOpenDomain.Enabled = True
    End If
  End Sub

  Private Sub ToolStripButtonLoadNewDomain_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles ToolStripButtonOpenDomain.Click
    Me.ToolStripButtonOpenDomain.Enabled = False

    If Me.ToolStripComboBoxDomainSelection.Text.Trim.Length > 0 Then
      Me.LaunchTreeViewRebuildThread()
    End If
  End Sub

  Private Sub ToolStripButtonStopDomainLoad_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles ToolStripButtonStopDomainLoad.Click
    If Me._treeViewThread IsNot Nothing AndAlso Me._treeViewThread.IsAlive Then
      Try
        Me.LabelLoadingDomain.Text = "Cancelling..."
        Me._treeViewThread.Abort()
      Catch ex As Threading.ThreadAbortException
        ' This is safe to ignore
      Catch ex As Exception
        MB("Error while terminating domain loading thread (" & ex.GetType.ToString & "): " & ex.Message, "Account Lockout Management", MessageBoxButtons.OK, MessageBoxIcon.Error)
      Finally
        TreeViewClearNodes(Me.TreeViewDomains)
        Me.ToolStripButtonOpenDomain.Visible = True
        Me.ToolStripButtonStopDomainLoad.Enabled = False
        Me.ToolStripButtonStopDomainLoad.Visible = False
      End Try
    End If
  End Sub

  Private Sub TreeViewDomains_AfterSelect(ByVal sender As Object, ByVal e As TreeViewEventArgs) Handles TreeViewDomains.AfterSelect
    Me.LaunchListViewRebuildThread(e.Node, Me.ToolStripButtonRecurse.Checked)
  End Sub
#End Region

#Region " Thread Functions "
  Private Sub LaunchTreeViewRebuildThread()
    ' Set the current domain
    Me._curDomain = Me.ToolStripComboBoxDomainSelection.Text.Trim

    ' Update the MRU
    For Each s As String In Me.ToolStripComboBoxDomainSelection.AutoCompleteCustomSource
      If s.Trim.ToUpper = Me._curDomain.Trim.ToUpper Then
        Me.ToolStripComboBoxDomainSelection.AutoCompleteCustomSource.Remove(s)
        Exit For
      End If
    Next
    Me.ToolStripComboBoxDomainSelection.AutoCompleteCustomSource.Add(Me._curDomain)

    ' Stop the thread if it is already running
    If Me._treeViewThread IsNot Nothing AndAlso Me._treeViewThread.IsAlive Then
      ' Thread is still active, abort
      Exit Sub
    End If

    ' Show loading sign
    Me.LabelLoadingDomain.Text = "Loading.  Please wait..."
    Me.LabelLoadingDomain.Enabled = True
    Me.LabelLoadingDomain.Visible = True

    ' Start the thread!
    Me._treeViewThread = New System.Threading.Thread(AddressOf RebuildDomainTreeViewThread)
    Me._treeViewThread.Start()

    ' Set button visibility
    Me.ToolStripButtonOpenDomain.Visible = False
    Me.ToolStripComboBoxDomainSelection.Enabled = False
    Me.ToolStripButtonStopDomainLoad.Enabled = True
    Me.ToolStripButtonStopDomainLoad.Visible = True
    Me.TreeViewDomains.Enabled = False
    Me.ListViewUsers.Enabled = False
    Me.ToolStripUsers.Enabled = False
  End Sub

  Private Sub LaunchListViewRebuildThread(ByVal n As TreeNode, ByVal recurse As Boolean)
    ' Set the current domain
    Me._curNode = n
    Me._recurseNodes = recurse

    ' Stop the thread if it is already running
    If Me._listViewThread IsNot Nothing AndAlso Me._listViewThread.IsAlive Then
      Try
        Me._listViewThread.Abort()
        While Me._listViewThread.IsAlive
        End While
      Catch ex As Threading.ThreadAbortException
      End Try
    End If

    ' Show loading sign
    Me.LabelLoadingUsers.Text = "Loading.  Please wait..."
    Me.LabelLoadingUsers.Enabled = True
    Me.LabelLoadingUsers.Visible = True

    ' Start the thread!
    Me._listViewThread = New System.Threading.Thread(AddressOf RebuildUserListViewThread)
    Me._listViewThread.Start()

    ' Set button visibility
    Me.ListViewUsers.Enabled = False
    Me.ToolStripUsers.Enabled = False
  End Sub


  Private Delegate Sub DelegateClearListView(ByVal view As ListView)
  Private Sub ClearListView(ByVal view As ListView)
    If view.InvokeRequired Then
      view.Invoke(New DelegateClearListView(AddressOf ClearListView), New Object() {view})
    Else
      view.Items.Clear()
    End If
  End Sub

  Private Delegate Sub DelegateAddToListView(ByVal view As ListView, ByVal lvi As ListViewItem)
  Private Sub AddToListView(ByVal view As ListView, ByVal lvi As ListViewItem)
    If view.InvokeRequired Then
      view.Invoke(New DelegateAddToListView(AddressOf AddToListView), New Object() {view, lvi})
    Else
      view.Items.Add(lvi)
    End If
  End Sub

  Private Delegate Sub DelegateAddRangeToListView(ByVal view As ListView, ByVal lvis() As ListViewItem)
  Private Sub AddRangeToListView(ByVal view As ListView, ByVal lvis() As ListViewItem)
    If view.InvokeRequired Then
      view.Invoke(New DelegateAddRangeToListView(AddressOf AddRangeToListView), New Object() {view, lvis})
    Else
      view.Items.AddRange(lvis)
    End If
  End Sub

  Private Delegate Sub DelegateTreeViewClearNodes(ByVal view As TreeView)
  Private Sub TreeViewClearNodes(ByVal view As TreeView)
    If view.InvokeRequired Then
      view.Invoke(New DelegateTreeViewClearNodes(AddressOf TreeViewClearNodes), New Object() {view})
    Else
      view.Nodes.Clear()
    End If
  End Sub

  Private Delegate Sub DelegateTreeViewAddRootNode(ByVal view As TreeView, ByVal node As TreeNode)
  Private Sub TreeViewAddRootNode(ByVal view As TreeView, ByVal node As TreeNode)
    If view.InvokeRequired Then
      view.Invoke(New DelegateTreeViewAddRootNode(AddressOf TreeViewAddRootNode), New Object() {view, node})
    Else
      view.Nodes.Add(node)
    End If
  End Sub

  Private Delegate Sub DelegateTreeViewSelectNode(ByVal view As TreeView, ByVal node As TreeNode)
  Private Sub TreeViewSelectNode(ByVal view As TreeView, ByVal node As TreeNode)
    If view.InvokeRequired Then
      view.Invoke(New DelegateTreeViewSelectNode(AddressOf TreeViewSelectNode), New Object() {view, node})
    Else
      view.SelectedNode = node
    End If
  End Sub

  Private Delegate Sub DelegateMB(ByVal message As String, ByVal caption As String, ByVal buttons As MessageBoxButtons, ByVal icon As MessageBoxIcon)
  Private Sub MB(ByVal message As String, ByVal caption As String, ByVal buttons As MessageBoxButtons, ByVal icon As MessageBoxIcon)
    If Me.InvokeRequired Then
      Me.Invoke(New DelegateMB(AddressOf MB), New Object() {message, caption, buttons, icon})
    Else
      MessageBox.Show(message, caption, buttons, icon)
    End If
  End Sub

  Private Delegate Sub DelegateSetControlVisible(ByVal ctl As Control, ByVal value As Boolean)
  Private Sub SetControlVisible(ByVal ctl As Control, ByVal value As Boolean)
    If ctl.InvokeRequired Then
      ctl.Invoke(New DelegateSetControlVisible(AddressOf SetControlVisible), New Object() {ctl, value})
    Else
      ctl.Visible = value
    End If
  End Sub

  Private Delegate Sub DelegateSetControlEnabled(ByVal ctl As Control, ByVal value As Boolean)
  Private Sub SetControlEnabled(ByVal ctl As Control, ByVal value As Boolean)
    If ctl.InvokeRequired Then
      ctl.Invoke(New DelegateSetControlEnabled(AddressOf SetControlEnabled), New Object() {ctl, value})
    Else
      ctl.Enabled = value
    End If
  End Sub

  Private Delegate Sub DelegateSetToolStripItemVisible(ByVal ts As ToolStrip, ByVal tsi As ToolStripItem, ByVal value As Boolean)
  Private Sub SetToolStripItemVisible(ByVal ts As ToolStrip, ByVal tsi As ToolStripItem, ByVal value As Boolean)
    If ts.InvokeRequired Then
      ts.Invoke(New DelegateSetToolStripItemVisible(AddressOf SetToolStripItemVisible), New Object() {ts, tsi, value})
    Else
      tsi.Visible = value
    End If
  End Sub

  Private Delegate Sub DelegateSetToolStripItemEnabled(ByVal ts As ToolStrip, ByVal tsi As ToolStripItem, ByVal value As Boolean)
  Private Sub SetToolStripItemEnabled(ByVal ts As ToolStrip, ByVal tsi As ToolStripItem, ByVal value As Boolean)
    If ts.InvokeRequired Then
      ts.Invoke(New DelegateSetToolStripItemEnabled(AddressOf SetToolStripItemEnabled), New Object() {ts, tsi, value})
    Else
      tsi.Enabled = value
    End If
  End Sub


#End Region


#Region " ListView Painting "
  Public Sub ListViewUsers_ColumnReordered(ByVal sender As Object, ByVal e As ColumnReorderedEventArgs) Handles ListViewUsers.ColumnReordered
    ' There are a few instances where when you reorder columns, the drawing event is fired before the
    ' columnheader information is updated, so column 0 (the listviewitem.subitem(0) which contains
    ' innaccurate information) will draw incorrectly.  by invalidating the listview a fraction of a
    ' second later, the column headers will be updated and the lsitview will be redraw correctly,
    ' with the user never experiencing any appreciable flicker or whatnot.
    Me.TimerInvalidateListView.Start()
  End Sub

  Private Sub TimerInvalidateListView_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerInvalidateListView.Tick
    Me.TimerInvalidateListView.Stop()
    Me.ListViewUsers.Invalidate()
  End Sub

  Public Sub ListViewUsers_DrawColumnHeader(ByVal sender As Object, ByVal e As System.Windows.Forms.DrawListViewColumnHeaderEventArgs) Handles ListViewUsers.DrawColumnHeader
    e.DrawDefault = True
  End Sub

  Public Sub ListViewUsers_DrawItem(ByVal sender As Object, ByVal e As System.Windows.Forms.DrawListViewItemEventArgs) Handles ListViewUsers.DrawItem
    e.DrawDefault = False

    ' Text/font Formatting flags used to make the items draw prettier
    Static fontFormat As New Drawing.StringFormat(Drawing.StringFormatFlags.NoWrap Or Drawing.StringFormatFlags.LineLimit)

    ' Determine the real left position and width of subitem(0) since the default window behavior is to return the width of the ENTIRE row
    Dim realLeft As Integer = 0
    Dim realWidth As Integer = Me.ListViewUsers.Columns(0).Width

    For i As Integer = 0 To Me.ListViewUsers.Columns.Count - 1
      If Me.ListViewUsers.Columns(i).DisplayIndex < Me.ListViewUsers.Columns(0).DisplayIndex Then
        realLeft += Me.ListViewUsers.Columns(i).Width
      End If
    Next

    ' Subtract realLeft from the scroll offset. "left" property of the bounds in the 0th subitem will
    ' represent this the offset (it's already a negative number when scrolling to the right)
    realLeft += e.Item.SubItems(0).Bounds.Left

    ' Draw the items
    For i As Integer = 0 To e.Item.SubItems.Count - 1
      Dim si As ListViewItem.ListViewSubItem = e.Item.SubItems(i)
      Dim rect As Drawing.Rectangle

      If i = 0 AndAlso e.Item.SubItems.Count > 1 Then
        ' Use the calculated REAL bounds, since LVSI(0) will represent entire row, always
        rect = New Drawing.Rectangle(realLeft, si.Bounds.Top, realWidth, si.Bounds.Height)
      Else
        rect = si.Bounds
      End If

      Select Case StringNullToEmpty(Me.ListViewUsers.Columns(i).Tag).ToUpper
        Case "CHECK"
          Dim imPos As New Drawing.Point(rect.X + (rect.Width / 2) - Me._bmHalfWidth, rect.Y + (rect.Height / 2) - Me._bmHalfHeight)

          If e.Item.Selected Then
            If Me.ListViewUsers.Focused Then
              ' SELECTED, FOCUSED
              e.Graphics.FillRectangle(Drawing.SystemBrushes.Highlight, rect)
            Else
              ' SELECTED, NOT FOCUSED
              e.Graphics.FillRectangle(Drawing.SystemBrushes.Control, rect)
            End If
          Else
            ' NOT SELECTED, NOT FOCUSED
            e.Graphics.FillRectangle(Drawing.SystemBrushes.Window, rect)
          End If

          Select Case si.Text
            Case SI_UNKNOWN
              e.Graphics.DrawImageUnscaled(Me._bmWarn, imPos)
            Case SI_CHECKED
              e.Graphics.DrawImageUnscaled(Me._bmCheck, imPos)
            Case SI_UNCHECKED
              ' Draw nothing
          End Select

        Case Else ' Display text
          Dim textRect As Drawing.Rectangle = GetTextRectangleFromBounds(rect)

          If e.Item.Selected Then
            If Me.ListViewUsers.Focused Then
              ' SELECTED, FOCUSED
              e.Graphics.FillRectangle(Drawing.SystemBrushes.Highlight, rect)
              Windows.Forms.TextRenderer.DrawText(e.Graphics, si.Text, si.Font, textRect, Drawing.SystemColors.HighlightText, Drawing.SystemColors.Highlight, fontFormat.FormatFlags)
            Else
              ' SELECTED, NOT FOCUSED
              e.Graphics.FillRectangle(Drawing.SystemBrushes.Control, rect)
              Windows.Forms.TextRenderer.DrawText(e.Graphics, si.Text, si.Font, textRect, Drawing.SystemColors.WindowText, Drawing.SystemColors.Control, fontFormat.FormatFlags)
            End If
          Else
            ' NOT SELECTED, NOT FOCUSED
            e.Graphics.FillRectangle(Drawing.SystemBrushes.Window, rect)
            Windows.Forms.TextRenderer.DrawText(e.Graphics, si.Text, si.Font, textRect, Drawing.SystemColors.WindowText, Drawing.SystemColors.Window, fontFormat.FormatFlags)
          End If
      End Select
    Next
  End Sub

  Public Sub ListViewUsers_DrawSubItem(ByVal sender As Object, ByVal e As System.Windows.Forms.DrawListViewSubItemEventArgs) Handles ListViewUsers.DrawSubItem
    e.DrawDefault = False
    e.DrawFocusRectangle(GetOuterRectangleFromBounds(e.Item.Bounds))
  End Sub

  Private Function GetOuterRectangleFromBounds(ByVal rect As Drawing.Rectangle) As Drawing.Rectangle
    Return New Drawing.Rectangle(rect.X - 1, rect.Y - 1, rect.Width + 2, rect.Height + 2)
  End Function

  Private Function GetTextRectangleFromBounds(ByVal rect As Drawing.Rectangle) As Drawing.Rectangle
    Return New Drawing.Rectangle(rect.X + 1, rect.Y + 2, rect.Width - 1, rect.Height - 2)
  End Function

  Private Function StringNullToEmpty(ByVal s As Object) As String
    If s Is Nothing Then
      Return ""
    Else
      Return s.ToString
    End If
  End Function

#End Region

End Class
